package top.codingfly.filter;

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.multipart.MultipartResolver;
import top.codingfly.filter.wrapper.HttpDataRequestWrapper;
import top.codingfly.filter.wrapper.HttpDataResponseWrapper;
import top.codingfly.utils.WebUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.*;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class LogFilter extends GenericFilterBean {
    public static String localAddr;
    public static String simpleLocalAddr;
    public static final Logger LOGGER = LoggerFactory.getLogger(LogFilter.class);
    public static final String REQUEST_TRACE_ID = "traceId";

    static {
        localAddr = getLocalAddr();
        simpleLocalAddr = localAddr.replaceAll("^\\d+\\.\\d+\\.(\\d+)\\.(\\d+)$", "$1$2");
    }

    public LogFilter() { }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String traceId = generateTraceId();
        MDC.put("logid", String.valueOf(Math.abs(traceId.hashCode())));
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        request.setAttribute(REQUEST_TRACE_ID, traceId);
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        MultipartResolver multipartResolver = WebApplicationContextUtils.getWebApplicationContext(getServletContext()).getBean(MultipartResolver.class);
        long start = System.currentTimeMillis();

        HttpDataResponseWrapper responseWrapper = new HttpDataResponseWrapper(response);
        String path = request.getRequestURI();
        String method = request.getMethod();
        String ip = WebUtils.getIpAddr(request);
        String body = null;
        HttpDataRequestWrapper requestWrapper = null;
        String ParameterMapString = JSON.toJSONString(request.getParameterMap());
        // 文件上传请求，不用 HttpDataRequestWrapper 封装请求流数据
        if (multipartResolver.isMultipart(request)==false) {
            requestWrapper = new HttpDataRequestWrapper(request);
            body = requestWrapper.getBodyString();
            request.setAttribute("bodyDataStr", body);
            if(StringUtils.isNotBlank(body)) {
                // 替换掉请求体JSON内容的换行符和tab空格，用于打印日志的时候，请求参数打印不换行
                body = body.replace("\r\n","").replace("\n","");
                // 如果是文件上传请求，postman不设置文件上传头部，后端会当作文本转换请求体的参数，日志打印会乱码
                // 因此文件请求大的，不打印日志，避免把文本当作请求体参数
                // 只打印请求体前5120个字符
                body = StringUtils.length(body) > 2056 ? body.substring(0, 2056) : body;
            }
            // 在这里决定是否打印请求体参数
            LOGGER.info(">>>beginReq,ip={},url={},method={},params={},body={},headers={}", ip, path, method, ParameterMapString, body, queryNeedPrintHeaders(request));
            filterChain.doFilter(requestWrapper, responseWrapper);
        } else {
            // 文件上传请求，不用 HttpDataRequestWrapper 封装请求流数据
            LOGGER.info(">>>beginReq,ip={},url={},method={},params={},body={},headers={}", ip, path, method, ParameterMapString, body, queryNeedPrintHeaders(request));
            filterChain.doFilter(servletRequest, responseWrapper);
        }
        String result = getRespBodyAsStr(responseWrapper);
        // 只打印前2056个字符，主要用于看code或者接口是否抛错，不是为了看所有响应内容
        result = StringUtils.isNotBlank(result) && result.length()>2056? result.substring(0, 2056):result;
        long end = System.currentTimeMillis();
        long cost = end - start;
        LOGGER.info("<<<completeReq,httpStatus={},url={},result={},cost={}ms", response.getStatus(), path, result, cost);
    }

    @Override
    public void destroy() {

    }

    public static Set<String> needPrintHeaders = new HashSet() {{
        add("signature");
        add("source");
        add("request-id");
        add("version");
        add("nonce");
        add("authorization");
        add("host");
        add("timestamp");
        add("user-agent");
        add("content-type");
    }};

    public static Map<String, String> queryNeedPrintHeaders(HttpServletRequest request) {
        Map<String, String> map = new HashMap();
        for (String key:needPrintHeaders) {
            String value = request.getHeader(key);
            if (StringUtils.isNotBlank(value)) {
                map.put(key, value);
            }
        }
        return map;
    }

    // 只解析返回类型为 application/json 的数据
    private String getRespBodyAsStr(HttpDataResponseWrapper response) {
        String contentType = response.getContentType();
        if (StringUtils.isBlank(contentType)) {
            // content-type 为空，不清楚该响应是文件流还是json格式，不提取请求体数据
            logger.info("响应数据的content-type为空，不解析响应内容");
            return "";
        }
        contentType = contentType.toLowerCase();
        if (contentType.contains("application/json")==false) {
            logger.info("响应数据的content-type不是application/json类型，不解析响应内容");
            return "";
        }
        try {
            byte[] body = response.getResponseData();
            if (body==null || body.length==0) {
                return "";
            }
            return new String(body, Charset.forName("UTF-8"));
        } catch (IOException e) {
            LOGGER.error("读取HttpServletResponse的body异常", e);
            return "";
        }

    }

    private String generateTraceId(){
        return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())
                + simpleLocalAddr
                + String.format("%06d", new Random().nextInt(1000000));
    }

    public static String getLocalAddr(){
        try {
            for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) {
                NetworkInterface item = e.nextElement();
                for (InterfaceAddress address : item.getInterfaceAddresses()) {
                    if (item.isLoopback() || !item.isUp()) {
                        continue;
                    }
                    if (address.getAddress() instanceof Inet4Address) {
                        Inet4Address inet4Address = (Inet4Address) address.getAddress();
                        return inet4Address.getHostAddress();
                    }
                }
            }
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException ex) {
            LOGGER.error("ServletUtil.getLocalIpByNetcard() with UnknownHostException: {}", ex, ex);
            return "";
        } catch (SocketException ex) {
            LOGGER.error("ServletUtil.getLocalIpByNetcard() with SocketException: {}", ex, ex);
            return "";
        }
    }

}
